package jmine.tec.utils.reflection.fast.asm;

import java.lang.reflect.Method;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import jmine.tec.utils.reflection.fast.FastMethod;
import jmine.tec.utils.reflection.fast.FastMethodNoArgs;
import jmine.tec.utils.reflection.fast.FastMethodSingleArgument;

import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;

/**
 * Compilador para metodos
 * 
 * @author takeshi
 */
public class FastMethodCompiler extends AbstractClassGenerator implements Opcodes {

    private static final Method MULTI_METHOD, SINGLE_ARG_METHOD, NO_ARG_METHOD;

    static {
        try {
            MULTI_METHOD = CompiledFastMethod.class.getMethod("invoke", Object.class, Object[].class);
            SINGLE_ARG_METHOD = CompiledFastMethod.class.getMethod("invokeSingleArgument", Object.class, Object.class);
            NO_ARG_METHOD = CompiledFastMethod.class.getMethod("invokeNoArgs", Object.class);
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    private final ConcurrentMap<Method, CompiledFastMethod> cache = new ConcurrentHashMap<Method, CompiledFastMethod>();

    /**
     * @param cl {@link OpenClassLoader}
     */
    public FastMethodCompiler(OpenClassLoader cl) {
        super(cl);
    }

    /**
     * Cria
     * 
     * @param name {@link String}
     * @param ifs as interfaces
     * @return {@link ClassWriter}
     */
    private static ClassWriter createClassGenerator(String name, String... ifs) {
        // System.out.println(ClassWriter.class.getProtectionDomain().getCodeSource().getLocation());
        ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        writer.visit(V1_5, ACC_PUBLIC + ACC_SUPER, name.replaceAll("[.]", "/"), null, Type.getInternalName(CompiledFastMethod.class), ifs);
        BytecodeHelper.createEmptyConstructor(writer, CompiledFastMethod.class);
        return writer;
    }

    /**
     * Compiles 1 argument
     * 
     * @param targetMethod {@link Method}
     * @return {@link CompiledFastMethod}
     */
    private CompiledFastMethod compileAndLoad1Arg(Method targetMethod) {
        String clName =
                FastClassGenerator.generateName(JMINE_TEC_GENERATED + "method1/" + getClassName(targetMethod.getDeclaringClass()) + "/"
                + targetMethod.getName());

        ClassWriter classWriter = createClassGenerator(clName, Type.getInternalName(FastMethodSingleArgument.class));
        overwriteMultiArgMethod(classWriter, targetMethod);
        overwriteSingleArgMethod(classWriter, targetMethod);
        classWriter.visitEnd();

        CompiledFastMethod fastMethod = this.loadClass(clName, classWriter.toByteArray(), CompiledFastMethod.class);
        fastMethod.setTargetMethod(targetMethod);
        return fastMethod;
    }

    /**
     * Compila um {@link FastMethod}
     * 
     * @param fm {@link FastMethod}
     * @return {@link CompiledFastMethod}
     */
    public CompiledFastMethod compileAndLoad(FastMethod fm) {
        if (fm instanceof CompiledFastMethod) {
            return (CompiledFastMethod) fm;
        }
        final Method method = fm.getMethod();
        CompiledFastMethod cached = this.cache.get(method);
        if (cached != null) {
            return cached;
        }
        synchronized (method) {
            cached = this.cache.get(method);
            if (cached != null) {
                return cached;
            }

            final CompiledFastMethod created;
            if (fm.getArity() == 0) {
                created = this.compileAndLoadMethodToNoArgs(method);
            } else if (fm.getArity() == 1) {
                created = this.compileAndLoad1Arg(method);
            } else {
                created = this.compileAndLoadMultipleArguments(method);
            }
            this.cache.put(method, created);
            return created;
        }
    }

    /**
     * @param method {@link Method}
     * @return {@link CompiledFastMethod}
     */
    private CompiledFastMethod compileAndLoadMultipleArguments(Method method) {
        String clName =
                FastClassGenerator.generateName(JMINE_TEC_GENERATED + "method" + method.getParameterTypes().length + "/"
                + getClassName(method.getDeclaringClass()) + "/" + method.getName());
        ClassWriter classWriter = createClassGenerator(clName);
        overwriteMultiArgMethod(classWriter, method);
        classWriter.visitEnd();
        CompiledFastMethod fastMethod = this.loadClass(clName, classWriter.toByteArray(), CompiledFastMethod.class);
        fastMethod.setTargetMethod(method);
        return fastMethod;
    }

    /**
     * compile
     * 
     * @param targetMethod {@link Method}
     * @return {@link CompiledFastMethod}
     */
    private CompiledFastMethod compileAndLoadMethodToNoArgs(Method targetMethod) {
        String clName =
                FastClassGenerator.generateName(JMINE_TEC_GENERATED + "method0/" + getClassName(targetMethod.getDeclaringClass()) + "/"
                + targetMethod.getName());

        ClassWriter classWriter = createClassGenerator(clName, Type.getInternalName(FastMethodNoArgs.class));
        overwriteMultiArgMethod(classWriter, targetMethod);
        overwriteNoArgMethod(classWriter, targetMethod);
        classWriter.visitEnd();
        CompiledFastMethod fastMethod = this.loadClass(clName, classWriter.toByteArray(), CompiledFastMethod.class);
        fastMethod.setTargetMethod(targetMethod);
        return fastMethod;
    }

    /**
     * Sobrescreve um metodo com multiplos argumentos
     * 
     * @param writer {@link ClassWriter}
     * @param targetMethod {@link Method}
     */
    private static void overwriteMultiArgMethod(ClassWriter writer, Method targetMethod) {
        MethodVisitor mv = BytecodeHelper.overwritePublicMethod(writer, MULTI_METHOD);
        BytecodeHelper.generateMethodCalling(mv, targetMethod, 1, 2);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * sobrescreve um metodo com um argumento
     * 
     * @param writer {@link ClassWriter}
     * @param targetMethod {@link Method}
     */
    private static void overwriteSingleArgMethod(ClassWriter writer, Method targetMethod) {
        MethodVisitor mv = BytecodeHelper.overwritePublicMethod(writer, SINGLE_ARG_METHOD);
        BytecodeHelper.generateSingleArgumentMethodCallReturnResult(mv, targetMethod, 1, 2);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

    /**
     * sobrescreve um metodo que nao recebe nenhum argumento
     * 
     * @param writer {@link ClassWriter}
     * @param targetMethod {@link Method}
     */
    private static void overwriteNoArgMethod(ClassWriter writer, Method targetMethod) {
        MethodVisitor mv = BytecodeHelper.overwritePublicMethod(writer, NO_ARG_METHOD);
        BytecodeHelper.generateNoArgMethodCallReturnResult(mv, targetMethod, 1);
        mv.visitMaxs(0, 0);
        mv.visitEnd();
    }

}
